WTForms 组件的介绍
1. WTForms 的介绍
- WTForms 是一个支持多个web框架的form组件,主要用于对用户请求数据进行验证(即: WTForms组件 和 Django中的 Form组件类似)
2. WTForms 的作用
- 生成 HTML 标签
- 对 form 表单进行验证
3. WTForms 的安装
pip3 install wtforms -i https://pypi.douban.com/simple # 使用豆瓣的镜像
创建对应的表单所需的HTML
- 通过 widgets 插件创建表单所需的HTML
- 常用的表单字段模块:
- simple
- core
- html5
- input -> 普通输入框
- 写法一
from wtforms import Form
from wtforms.fields import simple
# 创建表单类
class IndexForm(Form):
name = simple.StringField(
label='用户名'
)
- 写法二
from wtforms import Form
from wtforms import widgets
from wtforms.fields import simple
# 创建表单类
class IndexForm(Form):
name = simple.StringField(
label='用户名',
widget=widgets.TextInput(),
)
- password -> 密码输入框
from wtforms import Form
from wtforms import widgets
from wtforms.fields import simple
# 创建表单类
class IndexForm(Form):
password = simple.StringField(
label='密码',
widget=widgets.PasswordInput()
)
- textarea -> 文本域
from wtforms import Form
from wtforms import widgets
from wtforms.fields import simple
# 创建表单类
class IndexForm(Form):
article = simple.StringField(
label='文章',
widget=widgets.TextArea(),
render_kw={'id': 'article', 'cols': '50', 'rows': '30'}
)
- file -> 选择文件 -> 单文件上传
from wtforms import Form
from wtforms import widgets
from wtforms.fields import simple
# 创建表单类
class IndexForm(Form):
img = simple.StringField(
label='单文件上传',
widget=widgets.FileInput()
)
- file -> 选择文件 -> 多文件上传
from wtforms import Form
from wtforms import widgets
from wtforms.fields import simple
# 创建表单类
class IndexForm(Form):
imgs = simple.StringField(
label='多文件上传',
widget=widgets.FileInput(multiple=True)
)
- hidden -> 隐藏域
from wtforms import Form
from wtforms import widgets
from wtforms.fields import simple
# 创建表单类
class IndexForm(Form):
uids = simple.StringField(
label='隐藏域',
widget=widgets.HiddenInput()
)
- submit -> 提交按钮
from wtforms import Form
from wtforms import widgets
from wtforms.fields import simple
# 创建表单类
class IndexForm(Form):
submit_btn = simple.StringField(
label='submit 提交按钮',
widget=widgets.SubmitInput()
)
- radio -> 单选按钮
from wtforms import Form
from wtforms.fields import core
# 创建表单类
class IndexForm(Form):
gender = core.RadioField(
label='性别',
choices=[(1, '男'), (2, '女')],
coerce=int # 将提交过来的数据转换为int类型,当视图函数获取该字段的值的时候,那么该字段的值的数据类型就是 int 类型
)
- select -> 下拉选框
from wtforms import Form
from wtforms.fields import core
# 创建表单类
class IndexForm(Form):
city = core.SelectField(
label='城市',
choices=[
('bj', '北京'),
('sh', '上海'),
]
)
- select -> 多选的下拉选框
from wtforms import Form
from wtforms.fields import core
# 创建表单类
class IndexForm(Form):
hobby = core.SelectMultipleField(
label='爱好',
choices=[
(1, '篮球'),
(2, '足球')
],
coerce=int # 将提交过来的数据转换为int类型,当视图函数获取该字段的值的时候,那么该字段的值的数据类型就是 int 类型
)
- checkbox -> 复选框
from wtforms import Form
from wtforms import widgets
from wtforms.fields import core
# 创建表单类
class IndexForm(Form):
favor = core.SelectMultipleField(
label='喜好',
choices=[
(1, '篮球'),
(2, '足球'),
],
widget=widgets.ListWidget(prefix_label=False),
option_widget=widgets.CheckboxInput(),
coerce=int # 将提交过来的数据转换为int类型,当视图函数获取该字段的值的时候,那么该字段的值的数据类型就是 int 类型
)
- checkbox -> 单选框
- 写法一
from wtforms import Form
from wtforms.fields import core
# 创建表单类
class IndexForm(Form):
protocol = core.BooleanField(
label='是否同意协议?'
)
- 写法二
from wtforms import Form
from wtforms import widgets
from wtforms.fields import core
# 创建表单类
class IndexForm(Form):
protocol = core.BooleanField(
label='是否同意协议?',
widget=widgets.CheckboxInput()
)
- input -> html5的邮箱表单
from wtforms import Form
from wtforms import widgets
from wtforms.fields import html5
# 创建表单类
class IndexForm(Form):
email = html5.EmailField(
label='邮箱',
widget=widgets.TextInput(input_type='email')
)
选择性标签实时更新问题
- 在使用选择性标签时,需要注意choices的值可以是从数据库中获取到的值,但是由于是静态字段,获取的值无法实时更新,需要重写构造方法从而实现choice实时更新
import pymysql
from wtforms import Form
from wtforms.fields import core
# 创建表单类
class IndexForm(Form):
city = core.SelectField(
label='城市',
choices=[],
coerce=int
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
conn = pymysql.connect(host='localhost', user='root', password='', database='school', charset='utf8')
cursor = conn.cursor()
sql = 'select * from city'
cursor.execute(sql)
result = cursor.fetchall() # ((1, '广州'), (2, '深圳'), (3, '东莞'))
cursor.close()
conn.close()
self.city.choices = result # 从数据库中获取数据,然后更新静态属性中的数据
常用的字段参数
- widget -> 插件
from wtforms import Form
from wtforms import widgets
from wtforms.fields import simple
class IndexForm(Form):
password = simple.StringField(
widget=widgets.PasswordInput()
)
- label -> 用于生成Label标签或显示内容
from wtforms import Form
from wtforms.fields import simple
class IndexForm(Form):
name = simple.StringField(
label='用户名'
)
- render_kw -> 用于定义相关的id class 或 html 中的自定义属性
from wtforms import Form
from wtforms import widgets
from wtforms.fields import simple
class IndexForm(Form):
password = simple.StringField(
label='密码',
widget=widgets.PasswordInput(),
render_kw={'id': 'password', 'class': 'blue_input', 'placeholder': '请输入密码'}
)
- validators -> 验证规则
- 不能为空
from wtforms import Form
from wtforms import widgets
from wtforms import validators
from wtforms.fields import simple
class IndexForm(Form):
username = simple.StringField(
label='用户名',
widget=widgets.TextInput(),
validators=[ # 验证规则
validators.DataRequired(message='用户不能为空')
]
)
- 长度验证
from wtforms import Form
from wtforms import widgets
from wtforms import validators
from wtforms.fields import simple
class IndexForm(Form):
username = simple.StringField(
label='用户名',
widget=widgets.TextInput(),
validators=[ # 验证规则
validators.Length(min=6, max=18, message='用户名长度必须大于%(min)d,且小于%(max)d')
]
)
- 正则验证
from wtforms import Form
from wtforms import widgets
from wtforms import validators
from wtforms.fields import simple
class IndexForm(Form):
password = simple.StringField(
label='密码',
widget=widgets.PasswordInput(),
validators=[ # 验证规则
validators.Regexp(
regex="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[$@$!%*?&])[A-Za-z\d$@$!%*?&]{8,}",
message='密码至少8个字符,至少1个大写字母,1个小写字母,1个数字和1个特殊字符'
)
]
)
- 判断两次密码是否一致的验证
from wtforms import Form
from wtforms import widgets
from wtforms import validators
from wtforms.fields import simple
class IndexForm(Form):
pwd = simple.StringField(
label='密码',
widget=widgets.PasswordInput(),
validators=[ # 验证规则
validators.DataRequired(message='密码不能为空'),
]
)
pwd_confirm = simple.PasswordField(
label='重复密码',
widget=widgets.PasswordInput(),
validators=[ # 验证规则
validators.DataRequired(message='重复密码不能为空'),
validators.EqualTo('pwd', message="两次密码输入不一致")
]
)
- 邮箱格式验证
from wtforms import Form
from wtforms import widgets
from wtforms import validators
from wtforms.fields import html5
class IndexForm(Form):
email = html5.EmailField(
label='邮箱',
widget=widgets.TextInput(input_type='email'),
validators=[ # 验证规则
validators.Email(message='邮箱格式错误')
]
)
- default -> 默认值
from wtforms import Form
from wtforms import widgets
from wtforms.fields import simple
from wtforms.fields import core
class IndexForm(Form):
# input 输入框
username = simple.StringField(
widget=widgets.TextInput(),
default='默认值'
)
# radio 单选框
gender = core.RadioField(
label='性别',
choices=[
(1, '男'),
(2, '女'),
],
default=2
)
# select 下拉框
city = core.SelectField(
label='城市',
choices=[
('bj', '北京'),
('sh', '上海'),
],
default='sh'
)
# select 多选下拉框
hobby = core.SelectMultipleField(
label='爱好',
choices=[
(1, '篮球'),
(2, '足球'),
],
default=[1, 2]
)
# checkbox 复选框
favor = core.SelectMultipleField(
label='喜好',
choices=[
(1, '篮球'),
(2, '足球'),
],
widget=widgets.ListWidget(prefix_label=False),
option_widget=widgets.CheckboxInput(),
default=[1, 2]
)
# checkbox 单选框
protocol = core.BooleanField(
label='是否同意协议?',
widget=widgets.CheckboxInput(),
default=True
)
模板相关
1. 将Form类所生成的HTML发送到模板中
# app.py
from flask import Flask, render_template, request
from wtforms import Form
from wtforms import validators
from wtforms import widgets
from wtforms.fields import simple
app = Flask(__name__)
class RegisterForm(Form):
username = simple.StringField(
label='用户名',
widget=widgets.TextInput(),
validators=[
validators.DataRequired(message='用户名不能为空'),
validators.Length(min=3, message='用户名长度不能小于%(min)d')
],
render_kw={'placeholder': '请输入用户名'}
)
password = simple.PasswordField(
label='密码',
validators=[
validators.DataRequired(message='密码不能为空')
],
widget=widgets.PasswordInput(),
render_kw={'placeholder': '请输入密码'}
)
@app.route('/registered', methods=['GET', 'POST'])
def registered():
form_obj = RegisterForm() # 实例化一个表单对象
if request.method == 'POST':
form_obj = RegisterForm(formdata=request.form) # 将post过来的数据再次传入Form类中,然后对数据进行验证,如果数据有误,所实例化出来的form_obj就会拿到该条数据有误的相关信息
if form_obj.validate(): # 判断post过来的数据是否有误
# 获取验证成功的数据
print(form_obj.username.data) # Kevin
print(form_obj.password.data) # 123
# 将通过验证的数据保存到数据库中
return '注册成功'
else:
print(form_obj.errors) # 存放着所有form字段错误信息,只有进行了验证 form_obj.validate() 才会有错误信息 -> {'username': ['用户名长度不能小于3'], 'password': ['密码不能为空']}
return render_template('registered.html', form_obj=form_obj)
if __name__ == '__main__':
app.run()
2. 自定义
<form method="post" novalidate>
<div class="form-group">
<!-- 获取label的名称 -->
<lable>{{ form_obj.username.label }}</lable>
<!-- 获取相关的表单标签 -->
{{ form_obj.username }}
<!-- 获取第一条验证过后的错误信息 -->
<span class="help-block">{{ form_obj.username.errors.0 }}</span>
</div>
<div class="form-group">
<!-- 获取label的名称 -->
<lable>{{ form_obj.password.label }}</lable>
<!-- 获取相关的表单标签 -->
{{ form_obj.password }}
<!-- 获取第一条验证过后的错误信息 -->
<span class="help-block">{{ form_obj.password.errors.0 }}</span>
</div>
<input type="submit" value="提交">
</form>
3. 循环生成生成表单所需的HTML
<form method="post" novalidate>
<!-- 直接对返回的form对象进行循环 -->
{% for field in form_obj %}
<p>{{ field.label }}:{{ field }} {{ field.errors.0 }}</p>
{% endfor %}
<input type="submit" value="提交">
</form>
4. 获取错误信息的注意事项
- 不要使用 form_obj.errors.字段名.序列号,因为当 form_obj.errors.字段名 没有该字段名的时候就会报错
- 获取错误信息的正确方式: form_obj.字段名.errors.序列号
- 错误示范
<!-- 获取所有字段的错误信息 -> {'password': ['密码不能为空'], 'username': ['用户名长度不能小于3']} -->
{{ form_obj.errors }}
<!-- 当 form_obj.errors 下没有 username 的错误信息那么就会报错 -->
{{ form_obj.errors.username.0 }}
- 正确示范
{{ form_obj.username.errors.0 }}
将对应的数据自动填充到表单中(即:修改页面)
- 将对应数据自动填写到表单中(即: 修改页面)
- data 参数接收一个字典(即: 查询数据后得到的字典)
# app.py
from flask import Flask, render_template
from wtforms import Form
from wtforms import validators
from wtforms import widgets
from wtforms.fields import simple
from wtforms.fields import core
app = Flask(__name__)
class UserInfoForm(Form):
username = simple.StringField(
label='用户名',
widget=widgets.TextInput(),
validators=[
validators.DataRequired(message='用户名不能为空'),
validators.Length(min=3, message='用户名长度不能小于%(min)d')
],
render_kw={'placeholder': '请输入用户名'}
)
gender = core.RadioField(
label='性别',
choices=[
(1, '男'),
(2, '女'),
]
)
city = core.SelectField(
label='城市',
choices=[
('bj', '北京'),
('sh', '上海'),
]
)
@app.route('/edit_user_info', methods=['GET'])
def edit_user_info():
user_info_data = {
'username': 'Kevin',
'gender': 1,
'city': 'sh'
}
form_obj = UserInfoForm(data=user_info_data) # 将查询到的数据传递给 data 参数,从而实现将对应数据自动填写到表单中(即: 修改页面)
return render_template('edit_user_info.html', form_obj=form_obj)
if __name__ == '__main__':
app.run()
csrf_token
- flask 默认没有 csrf_token,但是 wtform 提供了 csrf_token
- 注意: wtform 所提供的 CSRF 的功能是不完整的,需要继承 csrf 类完善它里面的功能
# app.py
from flask import Flask, render_template, request
from wtforms import Form
from wtforms import validators
from wtforms import widgets
from wtforms.csrf.core import CSRF
from wtforms.fields import simple
from hashlib import md5
app = Flask(__name__)
# 继承 CSRF,完善 CSRF 的功能
class MyCSRF(CSRF):
def setup_form(self, form):
self.csrf_context = form.meta.csrf_context()
self.csrf_secret = form.meta.csrf_secret
return super(MyCSRF, self).setup_form(form)
# 生成 csrf_token(即:随机字符串)
def generate_csrf_token(self, csrf_token):
gid = self.csrf_secret + self.csrf_context
token = md5(gid.encode('utf-8')).hexdigest()
return token
# 当用户提交数据的时候,验证发送过来的csrf_token是否正确
def validate_csrf_token(self, form, field):
print(field.data, field.current_token)
if field.data != field.current_token:
raise ValueError('csrf_token 验证失败')
class LoginForm(Form):
username = simple.StringField(
label='用户名',
widget=widgets.TextInput(),
validators=[
validators.DataRequired(message='用户名不能为空'),
validators.Length(min=3, message='用户名长度不能小于%(min)d')
],
render_kw={'placeholder': '请输入用户名'}
)
password = simple.PasswordField(
label='密码',
validators=[
validators.DataRequired(message='密码不能为空')
],
widget=widgets.PasswordInput(),
render_kw={'placeholder': '请输入密码'}
)
class Meta:
# ----------- csrf_token -----------
csrf = True # 是否自动生成CSRF标签
csrf_field_name = 'csrf_token' # 生成CSRF标签name
csrf_secret = 'xxxxxx' # 自动生成标签的值,加密用的csrf_secret
csrf_context = lambda x: request.url # 自动生成标签的值,加密用的csrf_context
csrf_class = MyCSRF # 生成和比较csrf标签所使用到的类
@app.route('/login', methods=['GET', 'POST'])
def login():
form_obj = LoginForm()
if request.method == 'POST':
form_obj = LoginForm(formdata=request.form) # 一定要使用回 WTForms 进行数据的验证,否则 csrf 验证会无效
if form_obj.validate():
return '登陆成功'
else:
print(form_obj.errors.get('csrf_token')) # 获取 csrf_token 的错误信息
return render_template('login.html', form_obj=form_obj)
if __name__ == '__main__':
app.run()
# login.html
<form method="post" novalidate>
{{ form_obj.csrf_token }}
<p>用户名: {{ form_obj.username }}</p>{{ form_obj.username.errors.0 }}
<p>密码: {{ form_obj.password }}</p>{{ form_obj.password.errors.0 }}
{{ form_obj.csrf_token.errors.0 }}
<input type="submit" value="提交">
</form>
钩子函数
- 什么是钩子函数: WTForms源码通过反射找到指定前缀的函数并且执行该函数,即: 该指定前缀的函数就是钩子函数
- 什么时候使用钩子函数: 当 WTForms 所提供的校验规则无法满足你的校验,那么可以使用钩子函数自定义校验规则
# app.py
from wtforms import Form
from wtforms import widgets
from wtforms import validators
from wtforms.fields import simple
class IndexForm(Form):
pwd = simple.StringField(
label='密码',
widget=widgets.PasswordInput(),
validators=[
validators.DataRequired(message='密码不能为空'),
]
)
pwd_confirm = simple.PasswordField(
label='重复密码',
widget=widgets.PasswordInput(),
validators=[
validators.DataRequired(message='重复密码不能为空')
]
)
# 定义钩子函数(validate_字段名() 方法),用来校验pwd_confirm字段
def validate_pwd_confirm(self, field):
"""
自定义pwd_confirm字段规则,例:与pwd字段是否一致(注意:Flask已经提供了比较两个字段的值是否一致的功能,这里只是为了演示钩子函数的作用)
参数:
field.data: pwd_confirm字段所接收到的值
self.data: 存储着通过检验的字段的数据
"""
if field.data != self.data['pwd']:
# raise validators.ValidationError("密码不一致") # 继续当前字段的后续验证(注意:这里的后续验证只的不是下一个字段的验证,而是当前字段的下一个验证,因为一个字段可能会有多个验证)
raise validators.StopValidation("密码不一致") # 不再继续当前字段的后续验证(注意:这里的后续验证只的不是下一个字段的验证,而是当前字段的下一个验证,因为一个字段可能会有多个验证)